39장 DOM


노드 객체들로 구성된 트리 자료구조를 DOM 이라 한다.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <ul>
      <li id="apple">Apple</li>
      <li id="banana">Banana</li>
      <li id="orange">Orange</li>
    </ul>
    <script src="app.js"></script>
  </body>
</html>

위 html 문서를 렌더링 엔진이 파싱하면.

Untitled 33.png|Untitled 33.png

문서노드

DOM 트리 최상위에 존재하는 루트 노드로 document 객체를 가리킨다.
전역 객체 window의 document 프로퍼티에 바인딩 되어 있다.

요소노드

HTML 요소를 가르키는 객체

어트리뷰트 노드

HTML 요소의 어트리뷰트를 가리키는 객체
요소노드와 연결되어 있다. 부모 노드와 연결되어 있지 않고 요소노드에만 연결
→ 어트리뷰트 노드에 접근하려면 요소노드에 접근해야한다.
요소노드의 형제도 아니고 자식도 아닌 그러한 노드

텍스트 노드

HTML 요소의 텍스트를 가리키는 객체.
요소노드의 자식 노드. 자식 노드를 가질 수 없는 리프 노드이다.
접근하려면 먼저 요소 노드에 접근해야한다.

노드 객체의 상속 구조

DOM을 구성하는 노드 객체는 빌트인 객체는 아니고 브라우저 호스트 객체이다.
어쨋든 객체여서 상속 구조를 갖는다.

Untitled 1 19.png|Untitled 1 19.png

만약 input 요소 노드가 있을 때, 아래와 같은 프로토타입 체인을 가진다.
Attachments/Picture/Pasted image 20240109170128.png|300
프로토타입 체인에 의해 아래와 같은 상속받게 된다.
Untitled 2 16.png|Untitled 2 16.png

DOM은 HTML 문서의 계층적 구조와 정보를 표현하는 것은 물론 노드 객체의 종류, 즉 노드 타입에 따라 필요한 기능을 DOM API로 제공한다.
이 DOM API를 통해서 HTML 의 구조나 내용 또는 스타일 등을 동적으로 조작할 수 있다.

요소 노드 취득

요소를 조작하려면 요소를 취득해야한다.
DOM API 에서는 요소 노드를 취득할 수 있는 메서드들을 제공한다.

id를 이용한 요소 노드 취득

Document.prototype.getElementById 로 id를 인수로 전달해서 하나의 요소 노드 탐색.
Document.prototype의 프로퍼티이므로 반드시 document를 통해 호출해야한다.

여러개 일 경우 첫 번째 요소노드만 반환
존재 하지 않을 경우 null 반환.

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="apple">Apple</li>
      <li id="banana">Banana</li>
      <li id="orange">Orange</li>
    </ul>
    <script>
      // id 값이 'banana'인 요소 노드를 탐색하여 반환한다.
      // 두 번째 li 요소가 파싱되어 생성된 요소 노드가 반환된다.
      const $elem = document.getElementById('banana');

      // 취득한 요소 노드의 style.color 프로퍼티 값을 변경한다.
      $elem.style.color = 'red';
    </script>
  </body>
</html>

HTML 요소에 id 부여하면 id 값과 동일한 전역 변수가 암묵적으로 선언되고 해당 노드 객체가 할당된다.

<!DOCTYPE html>
<html>
  <body>
    <div id="foo"></div>
    <script>
      // id 값과 동일한 이름의 전역 변수가 암묵적으로 선언되고 해당 노드 객체가 할당된다.
      console.log(foo === document.getElementById('foo')); // true

      // 암묵적 전역으로 생성된 전역 프로퍼티는 삭제되지만 전역 변수는 삭제되지 않는다.
      delete foo;
      console.log(foo); // <div id="foo"></div>
    </script>
  </body>
</html>

단, id값과 동일한 이름의 전역 변수가 이미 선언되어 있으면 노드 객체가 할당되지 않는다.

태그 이름을 이용한 요소 노드 취득

Document.prototype/Element.prototype.getElementsByTagName 메서드는 인수로 전달한 태그이름을 갖는 모든 요소 노드들을 탐색하여 반환
DOM 컬렉션 객체인 HTMLCollection 객체 반환
HTMLCollection 객체는 유사 배열 객체이면서 이터러블이다. (34장 이터러블)

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li id="apple">Apple</li>
      <li id="banana">Banana</li>
      <li id="orange">Orange</li>
    </ul>
    <script>
      // 태그 이름이 li인 요소 노드를 모두 탐색하여 반환한다.
      // 탐색된 요소 노드들은 HTMLCollection 객체에 담겨 반환된다.
      // HTMLCollection 객체는 유사 배열 객체이면서 이터러블이다.
      const $elems = document.getElementsByTagName('li');

      // 취득한 모든 요소 노드의 style.color 프로퍼티 값을 변경한다.
      // HTMLCollection 객체를 배열로 변환하여 순회하며 color 프로퍼티 값을 변경한다.
      [...$elems].forEach(elem => { elem.style.color = 'red'; });
    </script>
  </body>
</html>

인수로 * 전달해서 모든 요소 노드를 취득할 수도 있다.

// 모든 요소 노드를 탐색하여 반환한다.
const $all = document.getElementsByTagName('*');
// -> HTMLCollection(8) [html, head, body, ul, li#apple, li#banana, li#orange, script, apple: li#apple, banana: li#banana, orange: li#orange]

Element.prototype.getElementsByTagName은 DOM 전체가 아닌 특정 요소 노드의 자손 노드 중에서 탐색한다.

class 를 이용한 요소 노드 취득

Document.prototype/Element.prototype.getElementsByClassName로 모든 요소 노드를 탐색해서 반환.
ByTagName과 같다.

CSS 선택자로 요소 노드 취득

querySelector 메서드는 인수로 전달한 CSS 선택자 만족시키는 하나의 요소 노드를 반환한다.
querySelector 메서드는 결과로 DOM 컬렉션 객체인 NodeList 객체를 반환한다.
NodeList 객체는 유사 배열 객체이면서 이터러블이다.

<!DOCTYPE html>
<html>
  <body>
    <ul>
      <li class="apple">Apple</li>
      <li class="banana">Banana</li>
      <li class="orange">Orange</li>
    </ul>
    <script>
      // class 어트리뷰트 값이 'banana'인 첫 번째 요소 노드를 탐색하여 반환한다.
      const $elem = document.querySelector('.banana');

      // 취득한 요소 노드의 style.color 프로퍼티 값을 변경한다.
      $elem.style.color = 'red';
      
	  // ul 요소의 자식 요소인 li 요소를 모두 탐색하여 반환한다.
	      const $elems = document.querySelectorAll('ul > li');
	      // 취득한 요소 노드들은 NodeList 객체에 담겨 반환된다.
	      console.log($elems); // NodeList(3) [li.apple, li.banana, li.orange]
    </script>
  </body>
</html>

qeurySelector 시리즈는 getEelement 시리즈보다 더 느리다.
하지만, CSS 선택자 문법을 사용해서 좀 더 구체적인 조건으로 요소 노드를 취득할 수 있고 일관된 방식으로 요소 노드를 취득할 수 있다는 장점이 있다.

특정 요소 노드 취득할 수 있는지 확인

Element.prototype.matches 메서드는 CSS 선택자로 특정 요소 노드를 취득할 수 있는지 확인한다.

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li class="apple">Apple</li>
      <li class="banana">Banana</li>
      <li class="orange">Orange</li>
    </ul>
  </body>
  <script>
    const $apple = document.querySelector('.apple');

    // $apple 노드는 '#fruits > li.apple'로 취득할 수 있다.
    console.log($apple.matches('#fruits > li.apple'));  // true

    // $apple 노드는 '#fruits > li.banana'로 취득할 수 없다.
    console.log($apple.matches('#fruits > li.banana')); // false
  </script>
</html>

이벤트 위임을 사용할 때 유용하다.

HTMLCollection과 NodeList

HTMLCollection과 NodeList 노드 객체의 상태 변화를 실시간으로 반영하는 살아있는 객체다.
HTMLCollection는 언제나 live 객체로 동작하지만, NodeList는 대부분 노드 객체의 상태 변화를 실시간으로 반영하지 않고 과거의 정적 상태를 유지하는 non-live 객체로 동작하고 때때로 live 객체로 동작한다.

HTMLCollection

<!DOCTYPE html>
<head>
  <style>
    .red { color: red; }
    .blue { color: blue; }
  </style>
</head>
<html>
  <body>
    <ul id="fruits">
      <li class="red">Apple</li>
      <li class="red">Banana</li>
      <li class="red">Orange</li>
    </ul>
    <script>
      // class 값이 'red'인 요소 노드를 모두 탐색하여 HTMLCollection 객체에 담아 반환한다.
      const $elems = document.getElementsByClassName('red');
      // 이 시점에 HTMLCollection 객체에는 3개의 요소 노드가 담겨 있다.
      console.log($elems); // HTMLCollection(3) [li.red, li.red, li.red]

      // HTMLCollection 객체의 모든 요소의 class 값을 'blue'로 변경한다.
      for (let i = 0; i < $elems.length; i++) {
        $elems[i].className = 'blue';
      }

      // HTMLCollection 객체의 요소가 3개에서 1개로 변경되었다.
      console.log($elems); // HTMLCollection(1) [li.red]
    </script>
  </body>
</html>

이 변경사항이 제대로 적용되지 않는다.
HTMLCoolection 객체는 live 객체여서 for문을 돌 때 실시간으로 요소가 제거되어 정확하게 동작할 수 없는 것이다.

for문을 역방향으로 순회하거나 while 문 등을 이용하여 회피할 수 있다.

// for 문을 역방향으로 순회
for (let i = $elems.length - 1; i >= 0; i--) {
  $elems[i].className = 'blue';
}

// while 문으로 HTMLCollection에 요소가 남아 있지 않을 때까지 무한 반복
let i = 0;
while ($elems.length > i) {
  $elems[i].className = 'blue';
}

HTMLCollection 객체를 사용하지 않는 방법도 있다.
배열로 변환하여 사용하면 된다.

// 유사 배열 객체이면서 이터러블인 HTMLCollection을 배열로 변환하여 순회
[...$elems].forEach(elem => elem.className = 'blue');

NodeList

앞서 봤던 문제를 해결하는 방법으로 querySelectorAll을 사용하는 방법도 있다.
NodeList 객체는 실시간으로 노드 객체의 상태 변경을 반영하지 않는 non-live 객체이기 때문

// querySelectorAll은 DOM 컬렉션 객체인 NodeList를 반환한다.
const $elems = document.querySelectorAll('.red');

// NodeList 객체는 NodeList.prototype.forEach 메서드를 상속받아 사용할 수 있다.
$elems.forEach(elem => elem.className = 'blue');

childNodes 프로퍼티가 반환하는 NodeList 객체는 HTMLCollection 객체처럼 live 객체로 동작한다

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
      <li>Banana</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById('fruits');

    // childNodes 프로퍼티는 NodeList 객체(live)를 반환한다.
    const { childNodes } = $fruits;
    console.log(childNodes instanceof NodeList); // true

    // $fruits 요소의 자식 노드는 공백 텍스트 노드(39.3.1절 "공백 텍스트 노드" 참고)를 포함해 모두 5개다.
    console.log(childNodes); // NodeList(5) [text, li, text, li, text]

    for (let i = 0; i < childNodes.length; i++) {
      // removeChild 메서드는 $fruits 요소의 자식 노드를 DOM에서 삭제한다.
      // (39.6.9절 "노드 삭제" 참고)
      // removeChild 메서드가 호출될 때마다 NodeList 객체인 childNodes가 실시간으로 변경된다.
      // 따라서 첫 번째, 세 번째 다섯 번째 요소만 삭제된다.
      $fruits.removeChild(childNodes[i]);
    }

    // 예상과 다르게 $fruits 요소의 모든 자식 노드가 삭제되지 않는다.
    console.log(childNodes); // NodeList(2) [li, li]
  </script>
</html>

HTMLCollection, NodeList 객체 모두 예상과 다르게 동작할 때가 있어서, 배열로 변환하여 사용하는 것도 좋은 방법이다.

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
      <li>Banana</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById('fruits');

    // childNodes 프로퍼티는 NodeList 객체(live)를 반환한다.
    const { childNodes } = $fruits;

    // 스프레드 문법을 사용하여 NodeList 객체를 배열로 변환한다.
    [...childNodes].forEach(childNode => {
      $fruits.removeChild(childNode);
    });

    // $fruits 요소의 모든 자식 노드가 모두 삭제되었다.
    console.log(childNodes); // NodeList []
  </script>
</html>

노드 탐색

요소 노드를 취득한 다음에, 취득 노드를 기준으로 주변 노드로 옮겨다니면서 노드를 탐색할 수 있다.

DOM 트리 상의 노드를 탐색할 수 있도록 Node, Element 인터페이스는 트리 탐색 프로퍼티를 제공한다.
Node 인터페이스의 메서드는 모든 노드(텍스트노드 포함)하는 반면, Element 인터페이스는 요소 노드만으로 작동한다.
Attachments/Picture/Pasted image 20240109183924.png|500

참고로 노드 탐색 프로퍼티들은 setter없이 getter만 존재하는 접근자 프로퍼티이다.

공백 텍스트 노드

HTML 요소 사이의 스페이스, 탭, 줄바꿈 등의 공백문자는 텍스트 노드를 생성한다.
이를 공백 텍스트 노드라고 한다.

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li class="apple">Apple</li>
      <li class="banana">Banana</li>
      <li class="orange">Orange</li>
    </ul>
  </body>
</html>

위와 같은 html 문서는 파싱되어 아래와 같은 DOM을 생성한다.
Attachments/Picture/Pasted image 20240109184224.png

자식 노드 탐색

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li class="apple">Apple</li>
      <li class="banana">Banana</li>
      <li class="orange">Orange</li>
    </ul>
  </body>
  <script>
    // 노드 탐색의 기점이 되는 #fruits 요소 노드를 취득한다.
    const $fruits = document.getElementById('fruits');

    // #fruits 요소의 모든 자식 노드를 탐색한다.
    // childNodes 프로퍼티가 반환한 NodeList에는 요소 노드뿐만 아니라 텍스트 노드도 포함되어 있다.
    console.log($fruits.childNodes);
    // NodeList(7) [text, li.apple, text, li.banana, text, li.orange, text]

    // #fruits 요소의 모든 자식 노드를 탐색한다.
    // children 프로퍼티가 반환한 HTMLCollection에는 요소 노드만 포함되어 있다.
    console.log($fruits.children);
    // HTMLCollection(3) [li.apple, li.banana, li.orange]

    // #fruits 요소의 첫 번째 자식 노드를 탐색한다.
    // firstChild 프로퍼티는 텍스트 노드를 반환할 수도 있다.
    console.log($fruits.firstChild); // #text

    // #fruits 요소의 마지막 자식 노드를 탐색한다.
    // lastChild 프로퍼티는 텍스트 노드를 반환할 수도 있다.
    console.log($fruits.lastChild); // #text

    // #fruits 요소의 첫 번째 자식 노드를 탐색한다.
    // firstElementChild 프로퍼티는 요소 노드만 반환한다.
    console.log($fruits.firstElementChild); // li.apple

    // #fruits 요소의 마지막 자식 노드를 탐색한다.
    // lastElementChild 프로퍼티는 요소 노드만 반환한다.
    console.log($fruits.lastElementChild); // li.orange
  </script>
</html>

자식 노드 존재 확인

Node.prototype.hasChildNodes 메서드 사용
자식 노드가 존재하면 true, 없으면 false 반환
텍스트 노드를 포함하여 확인한다.

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
    </ul>
  </body>
  <script>
    // 노드 탐색의 기점이 되는 #fruits 요소 노드를 취득한다.
    const $fruits = document.getElementById('fruits');

    // #fruits 요소에 자식 노드가 존재하는지 확인한다.
    // hasChildNodes 메서드는 텍스트 노드를 포함하여 자식 노드의 존재를 확인한다.
    console.log($fruits.hasChildNodes()); // true
  </script>
</html>

요소 노드가 존재하려면 children.length 또는 element.childElementCount를 사용한다.

부모 노드 탐색

Node.prototype.parentNode 프로퍼티 사용

형제 노드 탐색

노드 정보 취득

노드 객체에 대한 정보를 취득할 수 있는 정보 로퍼티

요소 노드의 텍스트 조작

nodeValue

노드 객체의 nodeValue는 접근자 프로퍼티로 setter와 getter 모두 존재
노드 객체의 값을 반환한다. 텍스트 노드의 텍스트!.
문서 노드나 요소 노드는 null 반환

<!DOCTYPE html>
<html>
  <body>
    <div id="foo">Hello</div>
  </body>
  <script>
    // 문서 노드의 nodeValue 프로퍼티를 참조한다.
    console.log(document.nodeValue); // null

    // 요소 노드의 nodeValue 프로퍼티를 참조한다.
    const $foo = document.getElementById('foo');
    console.log($foo.nodeValue); // null

    // 텍스트 노드의 nodeValue 프로퍼티를 참조한다.
    const $textNode = $foo.firstChild;
    console.log($textNode.nodeValue); // Hello
  </script>
</html>

요소 노드의 텍스트를 변경하려면 다음과 같은 순서의 처리

  1. 요소 노드 취득. 요소노드의 텍스트 노드 탐색
  2. 텍스트 노드의 nodeVaule 프로퍼티로 텍스트 노드 값 변경
<!DOCTYPE html>
<html>
  <body>
    <div id="foo">Hello</div>
  </body>
  <script>
    // 1. #foo 요소 노드의 자식 노드인 텍스트 노드를 취득한다.
    const $textNode = document.getElementById('foo').firstChild;

    // 2. nodeValue 프로퍼티를 사용하여 텍스트 노드의 값을 변경한다.
    $textNode.nodeValue = 'World';

    console.log($textNode.nodeValue); // World
  </script>
</html>

textContent

요소 노드의 텍스트와 모든 자식 노드의 텍스트를 모두 취득하거나 변경한다.

<!DOCTYPE html>
<html>
  <body>
    <div id="foo">Hello <span>world!</span></div>
  </body>
  <script>
    // #foo 요소 노드의 텍스트를 모두 취득한다. 이때 HTML 마크업은 무시된다.
    console.log(document.getElementById('foo').textContent); // Hello world!
  </script>
</html>

Attachments/Picture/Pasted image 20240109211453.png|500

nodeValue로 자식 노드의 텍스트 까지 모두 취득하려면 불편하다.
만일 자식 요소 노드가 없다면 같은 결과를 반환하긴 한다.

textContent 프로퍼티에 문자열을 할당하면 모든 자식 노드가 제거되고 할당한 문자열이 텍스트로 추가된다. 이 과정에서 HTML 마크업도 파싱되지 않는다.
Attachments/Picture/Pasted image 20240109211855.png|500

innerTexxt

비슷한 동작을 하는 innerText 프로퍼티가 있지만, 사용하지 않는 편이 좋다.

  • css에 순종적이다. 예를 들어 visibility : hidden 이면 텍스트를 반환하지 않는다.
  • css를 고려해서 textContent 프로퍼티보다 느리다.

DOM 조작

DOM에 새로운 노드를 추가하거나 삭제 또는 변경할 수가 있다.
DOM 조작에 의해 새로운 노드가 추가되거나 삭제되면 리플로우와 리페인트가 발생해서 성능에 영향을 주기도 한다.

innerHTML

요소 노드의 HTML 마크업을 취득하거나 변경한다.

<!DOCTYPE html>
<html>
  <body>
    <div id="foo">Hello <span>world!</span></div>
  </body>
  <script>
    // #foo 요소의 콘텐츠 영역 내의 HTML 마크업을 문자열로 취득한다.
    console.log(document.getElementById('foo').innerHTML);
    // "Hello <span>world!</span>"
  </script>
</html>

innerHTML에 문자열 할당하면 HTML로 파싱되어 DOM반영된다.
Attachments/Picture/Pasted image 20240109213648.png|500

innerHTML에 악성 script를 삽입하는 공격을 XSS라고 한다.
HTML5에서는 프로퍼티로 삽입된 script 요소 내의 js 코드를 실행하지 않는다.
하지만, img 태그에 에러 이벤트를 발생시켜서 js 코드가 실행되게하는 공격방법도 있따.

<!DOCTYPE html>
<html>
  <body>
    <div id="foo">Hello</div>
  </body>
  <script>
    // 에러 이벤트를 강제로 발생시켜서 자바스크립트 코드가 실행되도록 한다.
    document.getElementById('foo').innerHTML
      = `<img src="x" onerror="alert(document.cookie)">`;
  </script>
</html>

innerHTML은 기존의 요소를 모두 삭제하고 생성하는 방식이여서 비효율적이다.

$fruits.innerHTML += '<li class="banana">Banana</li>';

insertAdjacentHTML

기존 요소를 제거하지 않으면서 위치를 지정해 새로운 요소를 삽입한다.
인수로 1. 생성 위치 2. HTML 문자열 받는다.
위치는 다음과 같다.
Attachments/Picture/Pasted image 20240109214126.png|300

노드 생성과 추가

DOM은 노드를 생성/삽입/삭제/치화하는 메스도도 제공한다.

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById('fruits');

    // 1. 요소 노드 생성
    const $li = document.createElement('li');

    // 2. 텍스트 노드 생성
    const textNode = document.createTextNode('Banana');

    // 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
    $li.appendChild(textNode);

    // 4. $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
    $fruits.appendChild($li);
  </script>
</html>

요소 노드 생성

Document.prototype.createElement(tagname) 메서드는 요소 노드 생성하고 반환.

// 1. 요소 노드 생성
const $li = document.createElement('li');

요소 노드를 생성할 뿐 추가하지는 않는다. 추가해줘야한다.
그리고 아무런 자식 노드를 가지고 있지 않아서 텍스트 노드도 없다.

텍스트 노드 생성

Document.prototype.createTextNode(text) 메서드는 텍스트 노드생성하고 반환.

// 2. 텍스트 노드 생성
const textNode = document.createTextNode('Banana');

텍스트 노드를 요소 노드의 자식 노드로 추가

Node.prototype.appendChild(childNode) 메서드로 자식 노드로 추가한다.

// 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
$li.appendChild(textNode);

요소 자식 노드에 자식 노드가 하나도 없는 경우에는 textContent 프로퍼티 사용하는 편이 간단하다.

// 텍스트 노드를 생성하여 요소 노드의 자식 노드로 추가
$li.appendChild(document.createTextNode('Banana'));

// $li 요소 노드에 자식 노드가 하나도 없는 위 코드와 동일하게 동작한다.
$li.textContent = 'Banana';

요소를 DOM 에 추가

// 4. $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
$fruits.appendChild($li);

결과로 다음과 같은 DOM 트리가 완성된다.
Attachments/Picture/Pasted image 20240109224210.png|300

위예제에서는 DOM에 한번 추가되므로 DOM은 한번 변경되고, 이때 리플로우와 리페인트가 실행된다.

복수의 노드 생성과 추가

여러개의 노드를 생성하고 DOM에 추가하려면 DOM이 여러번 변경되고 그렇게 되면 많은 오버헤드가 발생한다.
그렇다고 div 같은 컨테이너로 하나로 감싸기에는 원하지 않는 구조가 될 수 있다.

이러한 문제를 DocumnetFragment 노드로 해결할 수 있다.
DocumnetFragment 노드는 노드 객체의 일종으로 부모노드가 없어서 기존 DOM과는 별도로 존재한다. 별도의 서브 DOM을 구성하여 기존 DOM에 추가하기 위한 용도로 사용된다.

DOM에 DocumnetFragment 노드를 추가하면 자신은 제거되고 자식 노드만 DOM에 추가된다.
Attachments/Picture/Pasted image 20240109224851.png

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits"></ul>
  </body>
  <script>
    const $fruits = document.getElementById('fruits');

    // DocumentFragment 노드 생성
    const $fragment = document.createDocumentFragment();

    ['Apple', 'Banana', 'Orange'].forEach(text => {
      // 1. 요소 노드 생성
      const $li = document.createElement('li');

      // 2. 텍스트 노드 생성
      const textNode = document.createTextNode(text);

      // 3. 텍스트 노드를 $li 요소 노드의 자식 노드로 추가
      $li.appendChild(textNode);

      // 4. $li 요소 노드를 DocumentFragment 노드의 마지막 자식 노드로 추가
      $fragment.appendChild($li);
    });

    // 5. DocumentFragment 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
    $fruits.appendChild($fragment);
  </script>
</html>

이렇게 DocumentFragment 노드를 사용하면 더 효율적이다.

노드 삽입

마지막 노드로 추가

Node.prototype.appendChild 메서드는 인수 노드를 마지막 자식 노드로 DOM에 추가한다.
언제나 마지막으로 추가된다.

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
      <li>Banana</li>
    </ul>
  </body>
  <script>
    // 요소 노드 생성
    const $li = document.createElement('li');

    // 텍스트 노드를 $li 요소 노드의 마지막 자식 노드로 추가
    $li.appendChild(document.createTextNode('Orange'));

    // $li 요소 노드를 #fruits 요소 노드의 마지막 자식 노드로 추가
    document.getElementById('fruits').appendChild($li);
  </script>
</html>

지정한 위치에 노드 삽입

Node.prototype.insertBefore(newNode, childNode)

첫번째 인수로 전달받은 노드를 두번째 인수로 전달받은 노드 앞에 삽입
이때, 두번째 인수 노드는 반드시 inserBefore 호출한 노드의 자식 노드여야 한다.
만약 두번째 인수가 null이면 appendChild 처럼 동작

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
      <li>Banana</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById('fruits');

    // 요소 노드 생성
    const $li = document.createElement('li');

    // 텍스트 노드를 $li 요소 노드의 마지막 자식 노드로 추가
    $li.appendChild(document.createTextNode('Orange'));

    // $li 요소 노드를 #fruits 요소 노드의 마지막 자식 요소 앞에 삽입
    $fruits.insertBefore($li, $fruits.lastElementChild);
    // Apple - Orange - Banana
  </script>
</html>

노드 이동

DOM에 이미 존재하는 노드를 appendChild 혹은 insertBefore로 DOM에 다시 추가하면 기존 노드 제거하고 새로운 위치에 추가한다.

<!DOCTYPE html>
<html>
  <body>
    <ul id="fruits">
      <li>Apple</li>
      <li>Banana</li>
      <li>Orange</li>
    </ul>
  </body>
  <script>
    const $fruits = document.getElementById('fruits');

    // 이미 존재하는 요소 노드를 취득
    const [$apple, $banana, ] = $fruits.children;

    // 이미 존재하는 $apple 요소 노드를 #fruits 요소 노드의 마지막 노드로 이동
    $fruits.appendChild($apple); // Banana - Orange - Apple

    // 이미 존재하는 $banana 요소 노드를 #fruits 요소의 마지막 자식 노드 앞으로 이동
    $fruits.insertBefore($banana, $fruits.lastElementChild);
    // Orange - Banana - Apple
  </script>
</html>

노드 복사

Node.prototype.cloneNode([deep: true | false])

노드의 사본 생성하여 반환.
deep 옵션으로 자손 노드를 포함할지 정할 수 있다.

노드 교체

Node.prototype.replaceChild(newChild, oldChild)

노드를 교체한다.

노드 삭제

Node.prototype.removeChild(child)

인수로 전달한 노드를 DOM에서 삭제

어트리뷰트

어트리뷰트 노드와 attributes 프로퍼티

HTML 문서가 파싱될 때 어트리뷰트는 어트리뷰트 노드로 변환되어 요소 노드와 연결된다.
어트리뷰트 하나당 하나의 어트리뷰트 노드가 생성되고, 이 어트리뷰트 노드의 참조는 유사 배열 객체이자 이터러블인 NamedNodeMap 객체에 담겨서 요소 노드의 attrbutes 프로퍼티에 저장된다.
Attachments/Picture/Pasted image 20240110142711.png

모든 어트리뷰트 노드는 요소 노드의 Element.prototype.attributes 프로퍼티로 취득 할 수 있다.
읽기 전용 접근자 프로퍼티이고, 모든 어트리뷰트 노드의 참조가 담긴 NamedNodeMap 객체를 반환한다.

<!DOCTYPE html>
<html>
<body>
  <input id="user" type="text" value="ungmo2">
  <script>
    // 요소 노드의 attribute 프로퍼티는 요소 노드의 모든 어트리뷰트 노드의 참조가 담긴 NamedNodeMap 객체를 반환한다.
    const { attributes } = document.getElementById('user');
    console.log(attributes);
    // NamedNodeMap {0: id, 1: type, 2: value, id: id, type: type, value: value, length: 3}

    // 어트리뷰트 값 취득
    console.log(attributes.id.value); // user
    console.log(attributes.type.value); // text
    console.log(attributes.value.value); // ungmo2
  </script>
</body>
</html>

HTML 어트리뷰트 조작

attribute 프로퍼티는 읽기 전용이고 어트리뷰트 값 취득하려면 attributes.id.value 와 같이 읽어와야해서 불편하다.
대신에 getAttribute/setAttribute 메서드를 사용할 수 있다.

<!DOCTYPE html>
<html>
<body>
  <input id="user" type="text" value="ungmo2">
  <script>
    const $input = document.getElementById('user');

    // value 어트리뷰트 값을 취득
    const inputValue = $input.getAttribute('value');
    console.log(inputValue); // ungmo2

    // value 어트리뷰트 값을 변경
    $input.setAttribute('value', 'foo');
    console.log($input.getAttribute('value')); // foo
  </script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
  <input id="user" type="text" value="ungmo2">
  <script>
    const $input = document.getElementById('user');

    // value 어트리뷰트의 존재 확인
    if ($input.hasAttribute('value')) {
      // value 어트리뷰트 삭제
      $input.removeAttribute('value');
    }

    // value 어트리뷰트가 삭제되었다.
    console.log($input.hasAttribute('value')); // false
  </script>
</body>
</html>

HTML 어트리뷰트 vs DOM 프로퍼티

요소 노드 객체에는 HTML 어트리뷰트에 대응하는 DOM 프로퍼티가 존재한다.
DOM 프로퍼티들은 HTML 속성값을 초기값으로 가진다.
Attachments/Picture/Pasted image 20240110163521.png
Attachments/Picture/Pasted image 20240110163911.png

노드 객체는 attributes 프로퍼티를 가지면서 각각의 attribute에 대응하는 DOM 프로퍼티도 가진다.
중복 관리되고 있는 것 처럼 보이지만 차이가 있다.

HTML 어트리뷰트의 역할을 HTML 요소의 초기 상태를 지정하는 것이다.
어트리뷰트는 렌덩링 될 때, attributes 프로퍼티와 DOM 프로퍼티로 변환되어 할당된다.
이후 요소 노드의 상태가 변경될 경우 DOM 프로퍼티 값이 바뀐다

즉, 요소 노드의 초기 상태는 어트리뷰트 노드가 관리하고, 요소 노드의 최신 상태는 DOM 프로퍼티가 관리한다.

왜 이렇게 2가지 상태를 관리하냐?

최신 상태를 관리하는 것은 당연하다.
웹페이지를 처음 표시하거나 새로고침할 때 초기 상태를 표시하기 위해서 초기 상태도 관리한다.

DOM 프로퍼티에 값을 할당하는 것은 HTML 요소의 최신 상태값을 변경하는 것을 의미한다.

<!DOCTYPE html>
<html>
<body>
  <input id="user" type="text" value="ungmo2">
  <script>
    const $input = document.getElementById('user');

    // DOM 프로퍼티에 값을 할당하여 HTML 요소의 최신 상태를 변경한다.
    $input.value = 'foo';
    console.log($input.value); // foo

    // getAttribute 메서드로 취득한 HTML 어트리뷰트 값, 즉 초기 상태 값은 변하지 않고 유지된다.
    console.log($input.getAttribute('value')); // ungmo2
  </script>
</body>
</html>

사용자 입력에 상태 변화가 없는 프로퍼티는 attribute 프로퍼티와 DOM 프로퍼티가 동일한 값을 유지한다.

<!DOCTYPE html>
<html>
<body>
  <input id="user" type="text" value="ungmo2">
  <script>
    const $input = document.getElementById('user');

    // id 어트리뷰트와 id 프로퍼티는 사용자 입력과 관계없이 항상 동일한 값으로 연동한다.
    $input.id = 'foo';

    console.log($input.id); // foo
    console.log($input.getAttribute('id')); // foo
  </script>
</body>
</html>

HTML 어트리뷰트와 DOM 프로퍼티의 대응관계

항상 HTML 어트리뷰트와 DOM 프로퍼티가 1:1대응하는 것은 아니다. 또 이름이 일치하지 않을 때가 있다.

DOM 프로퍼티 값의 타입

getAttribute 메서드로 취득한 어트리뷰트 값은 문자열이지만, DOM 프로퍼티 값은 문자열이 아닐 수도 있다.

<!DOCTYPE html>
<html>
<body>
  <input type="checkbox" checked>
  <script>
    const $checkbox = document.querySelector('input[type=checkbox]');

    // getAttribute 메서드로 취득한 어트리뷰트 값은 언제나 문자열이다.
    console.log($checkbox.getAttribute('checked')); // ''

    // DOM 프로퍼티로 취득한 최신 상태 값은 문자열이 아닐 수도 있다.
    console.log($checkbox.checked); // true
  </script>
</body>
</html>

data 어트리뷰트와 dataset 프로퍼티

data 어트리뷰트와 dataset 프로퍼티 사용해서 사용자가 정의한 어트리뷰트와 JS 간의 데이터 교환을 할 수 있다.
data- 접두사 다음에 임의의 이름을 붙여 사용한다.

HTMLElement.dataset 프로퍼티는 HTML 요소의 모든 data 어트리뷰트의 정보를 제공하는 DOMStringMap 객체를 반환한다.
이름은 카멜케이스로 변환된다.

<!DOCTYPE html>
<html>
<body>
  <ul class="users">
    <li id="1" data-user-id="7621" data-role="admin">Lee</li>
    <li id="2" data-user-id="9524" data-role="subscriber">Kim</li>
  </ul>
  <script>
    const users = [...document.querySelector('.users').children];

    // user-id가 '7621'인 요소 노드를 취득한다.
    const user = users.find(user => user.dataset.userId === '7621');
    // user-id가 '7621'인 요소 노드에서 data-role의 값을 취득한다.
    console.log(user.dataset.role); // "admin"

    // user-id가 '7621'인 요소 노드의 data-role 값을 변경한다.
    user.dataset.role = 'subscriber';
    // dataset 프로퍼티는 DOMStringMap 객체를 반환한다.
    console.log(user.dataset); // DOMStringMap {userId: "7621", role: "subscriber"}
  </script>
</body>
</html>

반대로 dataset 프로퍼티에 추가할 수도 있다.
이름이 케밥 케이스로 변환된다

<!DOCTYPE html>
<html>
<body>
  <ul class="users">
    <li id="1" data-user-id="7621">Lee</li>
    <li id="2" data-user-id="9524">Kim</li>
  </ul>
  <script>
    const users = [...document.querySelector('.users').children];

    // user-id가 '7621'인 요소 노드를 취득한다.
    const user = users.find(user => user.dataset.userId === '7621');

    // user-id가 '7621'인 요소 노드에 새로운 data 어트리뷰트를 추가한다.
    user.dataset.role = 'admin';
    console.log(user.dataset);
    /*
    DOMStringMap {userId: "7621", role: "admin"}
    -> <li id="1" data-user-id="7621" data-role="admin">Lee</li>
    */
  </script>
</body>
</html>

스타일

인라인 스타일 조작

HTMLElement.prototype.style 프로퍼티로 인라인 스타일을 추가, 변경할 수 있다.

<!DOCTYPE html>
<html>
<body>
  <div style="color: red">Hello World</div>
  <script>
    const $div = document.querySelector('div');

    // 인라인 스타일 취득
    console.log($div.style); // CSSStyleDeclaration { 0: "color", ... }

    // 인라인 스타일 변경
    $div.style.color = 'blue';

    // 인라인 스타일 추가
    $div.style.width = '100px';
    $div.style.height = '100px';
    $div.style.backgroundColor = 'yellow';
  </script>
</body>
</html>

style 프로퍼티를 참조하면 CSSStyleDeclaration 타입 객체가 반환된다.
다양한 CSS 프로퍼티에 대응하는 프로퍼티를 가지고 있다.
프로퍼티는 카멜 케이스를 따르다.

$div.style.backgroundColor = 'yellow';
// 대괄호 표기로 케밥 케이스 사용가능
$div.style['background-color'] = 'yellow';

클래스 조작

className

Element.prototype.className은 HTML 요소의 class 값 취득하거나 변경한다.
class가 여러개일 경우에도 문자열로 반환

classList

Element.prototype.classList 프로퍼티는 class 정보담은 DOMTokenList 객체 반환

<!DOCTYPE html>
<html>
<head>
  <style>
    .box {
      width: 100px; height: 100px;
      background-color: antiquewhite;
    }
    .red { color: red; }
    .blue { color: blue; }
  </style>
</head>
<body>
  <div class="box red">Hello World</div>
  <script>
    const $box = document.querySelector('.box');

    // .box 요소의 class 어트리뷰트 정보를 담은 DOMTokenList 객체를 취득
    // classList가 반환하는 DOMTokenList 객체는 HTMLCollection과 NodeList와 같이
    // 노드 객체의 상태 변화를 실시간으로 반영하는 살아 있는(live) 객체다.
    console.log($box.classList);
    // DOMTokenList(2) [length: 2, value: "box blue", 0: "box", 1: "blue"]

    // .box 요소의 class 어트리뷰트 값 중에서 'red'만 'blue'로 변경
    $box.classList.replace('red', 'blue');
  </script>
</body>
</html>

DOMTokenList 객체는 유사 배열 이면서 이터러블이다.
다음과 같은 메서드들을 지원한다.

요소에 적용되어 있는 CSS 스타일 참조

window.getComputedStyle(element[, pseudo])

노드의 평가된 스타일을 CSSStyleDeclaration 객체에 담아 반환.
평가된 스타일은 모든 스타일이 조합된 최종 스타일을 말한다.

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      color: red;
    }
    .box {
      width: 100px;
      height: 50px;
      background-color: cornsilk;
      border: 1px solid black;
    }
  </style>
</head>
<body>
  <div class="box">Box</div>
  <script>
    const $box = document.querySelector('.box');

    // .box 요소에 적용된 모든 CSS 스타일을 담고 있는 CSSStyleDeclaration 객체를 취득
    const computedStyle = window.getComputedStyle($box);
    console.log(computedStyle); // CSSStyleDeclaration

    // 임베딩 스타일
    console.log(computedStyle.width); // 100px
    console.log(computedStyle.height); // 50px
    console.log(computedStyle.backgroundColor); // rgb(255, 248, 220)
    console.log(computedStyle.border); // 1px solid rgb(0, 0, 0)

    // 상속 스타일(body -> .box)
    console.log(computedStyle.color); // rgb(255, 0, 0)

    // 기본 스타일
    console.log(computedStyle.display); // block
  </script>
</body>
</html>

reference